<?php
/*
File: mooremote.php

License:
	MIT-style license

Author:
	Jan Kassens

Note:
	magic_quotes_gpc needs to be turned off.
*/
/*
Class: MooRemote
PHP Class to register PHP methods and functions and process JS requests.
*/
class MooRemote{

	private $functions = array();
	private $classes = array();

	private $options;

	/*
	Constructor: __construct

	Parameters:
		options - array, see options below
	
	Options:
		url - url for the RPC calls. Defaults to the current url.
	*/
	
	public function __construct($options = array()){
		$this->options = $options;
	}

	/*
	Method: getJavascript
		Creates the JS code needed to initialize mooRemote in your page.
		<mooremote.js> needs to be included first.

	Parameters:
		tag - boolean; Wheather or not to surround the JS code with a script tag. Defaults to true.
		
	Returns:
		A string to be inserted into the page.
	*/
	
	public function getJavascript($tag = true){
		$functions = $this->functions;
		foreach($this->classes as $class => $methods){
			unset($methods['%object']);
			$functions[] = array('klass' => $class, 'methods' => $methods);
		}
		$return = "var mooRemote=new MooRemote(" . json_encode($functions);
		if(count($this->options) > 0) $return .= "," . json_encode($this->options);
		$return .= ");";
		if($tag) $return = "<script type=\"text/javascript\">$return</script>";
		return $return;
	}
	
	/*
	Method: processRequest
		If there is a request from JS it processes this request and by default stops PHP excecution afterwards.

	Parameters:
		exit - boolean; If set to false it does not stop script excecution after a processing a request. Defaults to true.
		
	Returns:
		True if a request was processed, false otherwise.
	*/
	
	public function processRequest($exit = true){
		if(!isset($_SERVER['HTTP_X_REQUESTED_WITH']) || $_SERVER['HTTP_X_REQUESTED_WITH'] != 'MooRequest') return false;
		set_error_handler(array($this, 'error'), E_ALL);
		try {
			$call = json_decode($_POST['call'], true);
			$method = $call['method'];
			$params = $call['params'];
			$callback = null;
			if (is_string($method)){ // function
				if(!in_array($method, $this->functions)) throw new Exception("Function $function is not registered.");
				$callback = $method;
			} else { // method
				$class = $method[0];
				$method = $method[1];
				if(!isset($this->classes[$class])) throw new Exception("Class $class is not registered.");
				if(!in_array($method, $this->classes[$class])) throw new Exception("Method $class::$method is not registered.");
				$callback = array(isset($this->classes[$class]['%object']) ? $this->classes[$class]['%object'] : $class, $method);
			}
			if(!is_callable($callback, false, $callable)) throw new Exception("Method is not callable.");
			ob_start();
			$result = call_user_func_array($callback, $params);
			$js = ob_get_contents();
			ob_end_clean();
			$this->response(array('result' => $result, 'script' => $js));
		} catch (Exception $e) {
			$this->error(1, $e->getMessage());
		}
		restore_error_handler();
		if($exit) exit;
		return true;
	}
	
	/*
	Method: registerFunction
	Registers a function to be callable from JS.
	
	Parameters:
		function - string; a user or build in function name
	
	Example:
		(code)
		$moo = new MooRemote()
		$moo->registerFunction('md5'); // Registers the build in function md5,
		                               // which can be called from JS with mooRemote.md5("myString");
		$moo->processRequests();       // Process requests from JS.
		(end code)
	*/

	public function registerFunction($function){
		$this->functions[] = $function;
	}
	
	/*
	Method: registerClass
	Registers a Class or an Object to be callable from JS.

	Parameters:
		name - string; Name to be used in JS, also name of the PHP class in case you dont pass an Object as second parameter.
		object - string, optional; Object instance to be called from JS.
		methods - array, optional; array of Strings representing the methods callable from JS. Defaults to every
		public (and in case you didn't pass an object also static) method.

	Example:
		(code)
		class TestClass{
			public static function foo(){
				// do something
			}
			public function bar(){
				// do something
			}
		}
		$moo = new MooRemote();
		$moo->registerClass('TestClass');  // Registers all public, static methods of TestClass, i.e. foo,
		                                   // which can be called from JS with mooRemote.TestClass.foo();
		$john = new TestClass();
		$moo->registerClass('joe', $john); // Registers all public methods of the TestClass instance $john as joe in JS.
		                                   // This enables mooRemote.joe.bar(); in JS.
		$moo->processRequests();           // Process requests from JS.
		(end code)
	*/
	public function registerClass($name, $object = null, $methods = null){
		$isObj = is_object($object);
		if (!$isObj) $methods = $object;
		if (!$methods) {
			$class = new ReflectionClass($isObj ? get_class($object) : $name);
			$methods = array();
			$mm = $class->getMethods();
			foreach($mm as $m){
				if ($m->isPublic() && ($isObj || $m->isStatic()) && !$m->isConstructor() && !$m->isDestructor()) $methods[] = $m->getName();
			}
		}
		if ($isObj) $methods['%object'] = $object;
		$this->classes[$name] = $methods;
	}
	
	public function error($errno, $errstr){
		$this->response(array('error' => $errstr));
		exit;
	}
	
	private function getMethods($class){
		$isObj = is_object($class);
		$class = new ReflectionClass($isObj ? get_class($class) : $class);
		$return = array();
		$methods = $class->getMethods();
		foreach($methods as $m){
			if ($m->isPublic() && ($isObj || $m->isStatic())) $return[] = $m->getName();
		}
		return $return;
	}

	// data: result, script, error;
	private function response($data){
		if(empty($data['script'])) unset($data['script']);
		echo json_encode($data);
	}

}
?>